package mspbots.qbd.online.core.client;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.log4j.Log4j2;
import mspbots.qbd.common.QueryParams;
import mspbots.qbd.common.RabbitKeys;
import mspbots.qbd.common.ResultSync;
import mspbots.qbd.online.annotation.ApiException;
import mspbots.qbd.online.oauth.AuthService;
import mspbots.qbd.online.oauth.TokenState;
import org.springframework.amqp.rabbit.core.RabbitMessagingTemplate;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.util.ObjectUtils;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;
import reactor.core.publisher.Mono;

import java.net.URI;
import java.util.List;

/**
 * mspbots.data.qbd.core.client.OnlineCllient
 *
 * @author Alex bob(https://github.com/vnobo)
 * @date Created by 2020/6/16
 */
@Log4j2
@Service
public class OnlineClient {

    private final AuthService authService;
    private final WebClient webClient;
    private final RabbitMessagingTemplate rabbitMessagingTemplate;
    private final ObjectMapper objectMapper;

    /**
     * Sandbox (development)	https://sandbox-quickbooks.api.intuit.com
     * Production	https://quickbooks.api.intuit.com
     *
     * @param builder                 client web build
     * @param authService             auth profitandloss
     * @param rabbitMessagingTemplate Rabbit send message
     * @param objectMapper            json mapper template
     */
    public OnlineClient(ClientProperties booksProperties,
                        WebClient.Builder builder,
                        AuthService authService,
                        RabbitMessagingTemplate rabbitMessagingTemplate,
                        ObjectMapper objectMapper) {

        this.rabbitMessagingTemplate = rabbitMessagingTemplate;
        this.objectMapper = objectMapper;
        this.authService = authService;
        this.webClient = builder.baseUrl(booksProperties.getBaseUrl())
                .defaultHeaders(httpHeaders -> httpHeaders.setAccept(List.of(MediaType.APPLICATION_JSON)))
                .defaultHeaders(httpHeaders -> httpHeaders.setContentType(MediaType.APPLICATION_JSON))
                .build();

    }

    public Mono<JsonNode> get(String path, QueryParams queryParams) {

        log.debug("Quick book online request {}, params: {} ", path, queryParams.getQuery());

        return Mono.subscriberContext().flatMap(ctx -> {
            TokenState authorization = ctx.get("tokenState");
            ResultSync resultSync = ctx.getOrDefault("resultSync", ResultSync.withDefault(path));
            assert resultSync != null;
            resultSync.setElapsedTime(System.currentTimeMillis());
            resultSync.setTenantId(authorization.getTenantId());
            resultSync.setParams(this.objectMapper.convertValue(queryParams, JsonNode.class));
            resultSync.setType("online");
            return this.webClient.get().uri(builder -> builder.path(path)
                    .queryParams(queryParams).build(authorization.getRealmId()))
                    .headers(httpHeaders -> httpHeaders.setBearerAuth(authorization.getAccessToken()))
                    .retrieve()
                    .onStatus(HttpStatus::isError, clientResponse -> clientResponse.bodyToMono(JsonNode.class)
                            .map(errNode -> ApiException.withMsg(clientResponse.statusCode(), errNode)))
                    .bodyToMono(JsonNode.class)
                    .doOnSuccess(resp -> this.handlerSuccessNext(resp, resultSync))
                    .onErrorResume(err -> this.handlerException(err, resultSync));
        });
    }

    public Mono<JsonNode> get(URI uri) {
        return Mono.subscriberContext().flatMap(ctx -> {
            TokenState authorization = ctx.get("tokenState");
            UriComponents uriComponents = UriComponentsBuilder.fromUri(uri).build();
            ResultSync resultSync = ctx.getOrDefault("resultSync",
                    ResultSync.withDefault(uriComponents.getPath()));
            assert resultSync != null;
            resultSync.setElapsedTime(System.currentTimeMillis());
            resultSync.setTenantId(authorization.getTenantId());
            resultSync.setParams(this.objectMapper.convertValue(uriComponents.getQueryParams(), JsonNode.class));
            resultSync.setType("online");
            return this.webClient.get().uri(uri)
                    .headers(httpHeaders -> httpHeaders.setBearerAuth(authorization.getAccessToken()))
                    .retrieve()
                    .onStatus(HttpStatus::isError, clientResponse -> clientResponse.bodyToMono(JsonNode.class)
                            .map(errNode -> ApiException.withMsg(clientResponse.statusCode(), errNode)))
                    .bodyToMono(JsonNode.class)
                    .doOnSuccess(resp -> this.handlerSuccessNext(resp, resultSync))
                    .onErrorResume(err -> this.handlerException(err, resultSync));
        });
    }

    public Mono<JsonNode> fetch(String path, QueryParams queryParams) {
        return this.query(path, queryParams)
                .map(jsonNode -> jsonNode.get("QueryResponse"));
    }


    private Mono<JsonNode> query(String path, QueryParams queryParams) {

        log.debug("Quick book online request {}, params: {} ", path, queryParams.getQuery());

        return Mono.subscriberContext().flatMap(ctx -> {
            TokenState authorization = ctx.get("tokenState");
            ResultSync resultSync = ctx.getOrDefault("resultSync", ResultSync.withDefault(path));
            assert resultSync != null;
            resultSync.setElapsedTime(System.currentTimeMillis());
            resultSync.setTenantId(authorization.getTenantId());
            resultSync.setParams(this.objectMapper.convertValue(queryParams, JsonNode.class));
            resultSync.setType("online");
            return this.webClient.get().uri(builder -> builder.path(path)
                    .queryParam("query", queryParams.getQuery())
                    .build(authorization.getRealmId()))
                    .headers(httpHeaders -> httpHeaders.setBearerAuth(authorization.getAccessToken()))
                    .retrieve()
                    .onStatus(HttpStatus::isError, clientResponse -> clientResponse.bodyToMono(JsonNode.class)
                            .map(errNode -> ApiException.withMsg(clientResponse.statusCode(), errNode)))
                    .bodyToMono(JsonNode.class)
                    .doOnSuccess(resp -> this.handlerSuccessNext(resp, resultSync))
                    .onErrorResume(err -> this.handlerException(err, resultSync));
        });
    }

    private Mono<JsonNode> handlerException(Throwable err, ResultSync resultSync) {

        if (err instanceof ApiException) {
            resultSync.setMsg(((ApiException) err).getMsg());
            resultSync.setMsg(((ApiException) err).getStatus());
        } else {
            resultSync.setMsg(err.getMessage());
            resultSync.status(HttpStatus.INTERNAL_SERVER_ERROR);
        }

        log.error("Request {} error action {}, elapsedTime {}, msg: {}",
                resultSync.getTenantId(), resultSync.getAction(),
                resultSync.getElapsedTime(), resultSync.getMsg());

        resultSync.setElapsedTime(System.currentTimeMillis() - resultSync.getElapsedTime());
        this.rabbitMessagingTemplate.convertAndSend(RabbitKeys.BASE_EXCHANGE_NAME,
                RabbitKeys.LOGS_TASK_QUEUE_KEY, resultSync);
        return Mono.error(err);
    }

    private void handlerSuccessNext(JsonNode resp, ResultSync resultSync) {
        JsonNode pageDetailsNode = resp.findValue("maxResults");
        if (!ObjectUtils.isEmpty(pageDetailsNode)) {
            resultSync.setCount(pageDetailsNode.intValue());
        } else {
            resultSync.setCount(resp.size());
        }
        resultSync.setElapsedTime(System.currentTimeMillis() - resultSync.getElapsedTime());

        log.info("Request {} success, action {}, elapsedTime {}, count {}",
                resultSync.getTenantId(), resultSync.getAction(),
                resultSync.getElapsedTime(), resultSync.getCount());

        this.rabbitMessagingTemplate.convertAndSend(RabbitKeys.BASE_EXCHANGE_NAME,
                RabbitKeys.LOGS_TASK_QUEUE_KEY, resultSync);
    }
}
